package uk.co.richardwkirk.funnels.model;
import java.util.ArrayList;
import java.util.LinkedList;

import uk.co.richardwkirk.funnels.FunnelSettings;


public class FunnelSystem {

	ArrayList<Funnel> funnels = null;
	LinkedList<Runner> buffer = new LinkedList<Runner>();
	LinkedList<Runner> overflow = new LinkedList<Runner>();
	int exitFunnel = 0;
	int fillFunnel = 0;
	int fillQueue = 0;
	int drainQueue = 0;
	
	// Timing
	long maxTimeInFunnel = 0;
	long totalTimeInFunnel = 0;
	int timedRunners = 0;
	
	FunnelSettings funnelSettings = null;
	
	public FunnelSystem(FunnelSettings settings)
	{
		// Create the funnels
		exitFunnel = 0;
		fillFunnel = 0;
		
		funnelSettings = settings;

		funnels = new ArrayList<Funnel>(funnelSettings.NUMBER);
		for (int i = 0; i < funnelSettings.NUMBER; ++i)
		{
			funnels.add(new Funnel(i, funnelSettings));
		}
	}

	public boolean isEmpty() {
		for (Funnel nextFunnel : funnels)
		{
			if (!nextFunnel.isEmpty()) {
				return false;
			}
		}
		return true;
	}
	
	private boolean isQueueEmpty(int drainQueue) {
		for (Funnel nextFunnel : funnels)
		{
			if (!nextFunnel.isQueueEmpty(drainQueue)) {
				return false;
			}
		}
		return true;
	}

	public ArrayList<Funnel> getFunnels() {
		return funnels;
	}

	public void addFinishers(LinkedList<Runner> nextFinishers) {
		for (Runner nextRunner : nextFinishers)
		{
			if (buffer.size() < funnelSettings.BUFFER_SIZE)
			{
				buffer.add(nextRunner);
			}
			else
			{
				overflow.add(nextRunner);
			}
		}
	}

	private Funnel getFillFunnel(int spaceRequired) {
		Funnel theFunnel = funnels.get(fillFunnel);
		if (theFunnel.hasSpace(spaceRequired))
		{
			return theFunnel;
		}
		else
		{
			return getNextFillFunnel(spaceRequired);
		}
	}
	
	private Funnel getNextFillFunnel(int spaceRequired) {	
		// Look for the next empty funnel
		int originalFillFunnel = fillFunnel; 
		fillFunnel = incrementFunnelNumber(fillFunnel);
		
		// Keep looking until we find a free funnel or come back to this one
		while (fillFunnel != originalFillFunnel) {
			Funnel nextFunnel = funnels.get(fillFunnel);
			if (nextFunnel.hasAvailableSpace()) {
				// We are moving to another funnel so increment our queue count
				++fillQueue;
				
				return nextFunnel;
			}
			fillFunnel = incrementFunnelNumber(fillFunnel);
		}
		return null;
	}

	private int incrementFunnelNumber(int funnelId) {
		int nextFunnel = funnelId + 1;
		if (nextFunnel == funnelSettings.NUMBER) {
			nextFunnel = 0;
		}
		return nextFunnel;
	}

	public Runner exit() {
		Funnel currentExitFunnel = getExitFunnel();
		Runner runner = currentExitFunnel.exit(drainQueue);
		if (currentExitFunnel.isQueueEmpty(drainQueue)) {
			moveToNextExitFunnel();
		}
		return runner;
	}

	private void moveToNextExitFunnel() {
		if (isEmpty()) {
			// If funnels system is empty we don't move
			return;
		}
		
		// Move to the next queue to drain
		++drainQueue;
		
		while (funnels.get(exitFunnel).isQueueEmpty(drainQueue)) {
			exitFunnel = incrementFunnelNumber(exitFunnel);
		}
	}

	private Funnel getExitFunnel() {
		return funnels.get(exitFunnel);
	}

	public void shift() {
		// Move everybody up
		for (Funnel funnel : funnels)
		{
			funnel.shiftUp();
		}
		
		// Drain the buffer
		if (buffer.size() > 0)
		{
			// Get a funnel to put the next runner into
			Funnel fillFunnel = getFillFunnel(1);
			if (fillFunnel != null) {
				try {
					fillFunnel.add(buffer.getFirst(), fillQueue);
					buffer.removeFirst();
				}
				catch (FullFunnelException e) {
					// Nothing required. Leave the runner in the buffer.
				}
			}
		}
		
		// Drain the overflow
		if (overflow.size() > 0 && buffer.size() < funnelSettings.BUFFER_SIZE) {
			buffer.add(overflow.getFirst());
			overflow.removeFirst();
		}
	}

	public int getOverflowSize() {
		return overflow.size();
	}

	public boolean hasOverflowed() {
		return (overflow.size() != 0);
	}

	public boolean isExitFunnel(Funnel funnel) {
		return funnel.getId() == exitFunnel;
	}

	public boolean isFillFunnel(Funnel funnel) {
		return funnel.getId() == fillFunnel;
	}

	public LinkedList<Runner> getBuffer() {
		return buffer;
	}

	public int getBufferSize() {
		return funnelSettings.BUFFER_SIZE;
	}

	public int getFillQueue() {
		return fillQueue;
	}

	public int getDrainQueue() {
		return drainQueue;
	}

	public void addTimeInQueue(long timeInQueue) {
		++timedRunners;
		totalTimeInFunnel += timeInQueue;
		if (timeInQueue > maxTimeInFunnel) {
			maxTimeInFunnel = timeInQueue;
		}
	}

	public long getAverageTimeSpent() {
		if (timedRunners > 0) {
			return (totalTimeInFunnel / timedRunners) / 1000;
		}
		else {
			return 0;
		}
	}
	
	public long getMaximumTimeSpent() {
		return maxTimeInFunnel / 1000;
	}

	
}
